package com.blank.shop.ssr.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 短链接小案例
 * <br/>Date 2021/9/8
 * <br/>Time 11:33:51
 *
 * @author _blank
 */
@Slf4j
@RestController
public class ShortUrlController {
    /**
     * 26 + 26 + 10 = 62
     */
    public static final char[] CHARS = {
            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
            'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
            'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
    };

    public final static String SHORT_URL_KEY = "short:url";

    @Value("${server.port}")
    private Integer serverPort;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    private HttpServletResponse httpServletResponse;

    /**
     * 长链接转换为短链接
     * 实现原理：长链接转换为短加密串key，然后存储在redis的hash结构中。
     */
    @GetMapping(path = {"/encode"})
    public String encode(String longUrl) {
        //一个长链接url转换为4个短加密串key
        final String[] keys = this.shortUrl(longUrl);
        //任意取出其中一个，我们就拿第一个
        final String shortUrlKey = keys[0];
        //用hash存储，key=加密串，value=原始url
        this.redisTemplate.opsForHash().put(SHORT_URL_KEY, shortUrlKey, longUrl);
        log.info("长链接:    {}    转换短链接: {}", longUrl, shortUrlKey);
        return "http://127.0.0.1:" + this.serverPort + "/decode/" + shortUrlKey;
    }

    /**
     * 重定向到原始的URL
     * 实现原理：通过短加密串KEY到redis找出原始URL，然后重定向出去
     */
    @GetMapping(value = "/decode/{shortUrlKey}")
    public void decode(@PathVariable String shortUrlKey) {
        // 到redis中把原始url找出来
        final String url = (String) this.redisTemplate.opsForHash().get(SHORT_URL_KEY, shortUrlKey);
        log.info("----真实长地址url: {}", url);
        try {
            //重定向到原始的url
            log.info("httpServletResponse,    {}    {}", this.httpServletResponse, this.httpServletResponse.hashCode());
            this.httpServletResponse.sendRedirect(url);
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    }

    /**
     * 一个长链接URL转换为4个短KEY
     * 思路：
     * 1)将长网址md5生成32位签名串,分为4段, 每段8个字节;
     * 2)对这四段循环处理, 取8个字节, 将他看成16进制串与0x3fffffff(30位1)与操作, 即超过30位的忽略处理;
     * 3)这30位分成6段, 每5位的数字作为字母表的索引取得特定字符, 依次进行获得6位字符串;
     * 4)总的md5串可以获得4个6位串; 取里面的任意一个就可作为这个长url的短url地址;
     * 当我们点击这6个字母的链接后，我们又可以跳转到原始的真实链接地址。
     */
    private String[] shortUrl(String url) {
        // 对传入网址进行 MD5 加密
        // Calculates the MD5 digest and returns the value as a 32 character hex string.
        final String md5HexEncryptResult = DigestUtils.md5DigestAsHex(url.getBytes());
        log.info("md5HexEncryptResult: {}", md5HexEncryptResult);

        // 切割为4组，每组8个字符, 32 = 4 *  8
        final String[] resUrl = new String[4];

        for (int i = 0; i < 4; i++) {
            // 取出8位字符串，md5 32位，按照8位一组字符,被切割为4组
            final String sTempSubString = md5HexEncryptResult.substring(i * 8, i * 8 + 8);
            final long md5ToHex = Long.parseLong(sTempSubString, 16);
            log.info("截取MD5下标{}到{}的结果是：{}，转换成16进制是：{}", i * 8, i * 8 + 8, sTempSubString, md5ToHex);

            final StringBuilder outChars = new StringBuilder();

            // 把加密字符按照8位一组16进制与 0x3FFFFFFF 进行位与运算
            // 这里需要使用 long 型来转换，因为 Inteper .parseInt() 只能处理 31 位 , 首位为符号位 , 如果不用 long ，则会越界
            long lHexLong = 0x3FFFFFFF & md5ToHex;
            for (int j = 0; j < 6; j++) {
                // 0x0000003D它的10进制是61，61代表最上面定义的chars数组长度62的0到61的坐标。
                // 0x0000003D & lHexLong进行位与运算，就是格式化为6位，即保证了index绝对是61以内的值
                final long index = 0x0000003D & lHexLong;
                // 按照下标index把从chars数组取得的字符逐个相加
                outChars.append(CHARS[(int) index]);
                // 每次循环按位移5位，因为30位的二进制，分6次循环，即每次右移5位
                lHexLong = lHexLong >> 5;
            }

            // 把字符串存入对应索引的输出数组,会产生一组6位字符串
            resUrl[i] = outChars.toString();
        }

        return resUrl;
    }

}
